!(function (win) {

    /**
     * FastDom
     *
     * Eliminates layout thrashing
     * by batching DOM read/write
     * interactions.
     *
     * @author Wilson Page <wilsonpage@me.com>
     * @author Kornel Lesinski <kornel.lesinski@ft.com>
     */

    'use strict';

    /**
     * Mini logger
     *
     * @return {Function}
     */
    var debug = 0 ? console.log.bind(console, '[fastdom]') : function () { };

    /**
     * Normalized rAF
     *
     * @type {Function}
     */
    var raf = win.requestAnimationFrame
        || win.webkitRequestAnimationFrame
        || win.mozRequestAnimationFrame
        || win.msRequestAnimationFrame
        || function (cb) { return setTimeout(cb, 16); };

    /**
     * Initialize a `FastDom`.
     *
     * @constructor
     */
    function FastDom() {
        var self = this;
        self.reads = [];
        self.writes = [];
        self.raf = raf.bind(win); // test hook
        debug('initialized', self);
    }

    FastDom.prototype = {
        constructor: FastDom,

        /**
         * We run this inside a try catch
         * so that if any jobs error, we
         * are able to recover and continue
         * to flush the batch until it's empty.
         *
         * @param {Array} tasks
         */
        runTasks: function (tasks) {
            debug('run tasks');
            var task; while (task = tasks.shift()) task();
        },

        /**
         * Adds a job to the read batch and
         * schedules a new frame if need be.
         *
         * @param  {Function} fn
         * @param  {Object} ctx the context to be bound to `fn` (optional).
         * @public
         */
        measure: function (fn, ctx) {
            debug('measure');
            var task = !ctx ? fn : fn.bind(ctx);
            this.reads.push(task);
            scheduleFlush(this);
            return task;
        },

        /**
         * Adds a job to the
         * write batch and schedules
         * a new frame if need be.
         *
         * @param  {Function} fn
         * @param  {Object} ctx the context to be bound to `fn` (optional).
         * @public
         */
        mutate: function (fn, ctx) {
            debug('mutate');
            var task = !ctx ? fn : fn.bind(ctx);
            this.writes.push(task);
            scheduleFlush(this);
            return task;
        },

        /**
         * Clears a scheduled 'read' or 'write' task.
         *
         * @param {Object} task
         * @return {Boolean} success
         * @public
         */
        clear: function (task) {
            debug('clear', task);
            return remove(this.reads, task) || remove(this.writes, task);
        },

        /**
         * Extend this FastDom with some
         * custom functionality.
         *
         * Because fastdom must *always* be a
         * singleton, we're actually extending
         * the fastdom instance. This means tasks
         * scheduled by an extension still enter
         * fastdom's global task queue.
         *
         * The 'super' instance can be accessed
         * from `this.fastdom`.
         *
         * @example
         *
         * var myFastdom = fastdom.extend({
         *   initialize: function() {
         *     // runs on creation
         *   },
         *
         *   // override a method
         *   measure: function(fn) {
         *     // do extra stuff ...
         *
         *     // then call the original
         *     return this.fastdom.measure(fn);
         *   },
         *
         *   ...
         * });
         *
         * @param  {Object} props  properties to mixin
         * @return {FastDom}
         */
        extend: function (props) {
            debug('extend', props);
            if (typeof props != 'object') throw new Error('expected object');

            var child = Object.create(this);
            mixin(child, props);
            child.fastdom = this;

            // run optional creation hook
            if (child.initialize) child.initialize();

            return child;
        },

        // override this with a function
        // to prevent Errors in console
        // when tasks throw
        catch: null
    };

    /**
     * Schedules a new read/write
     * batch if one isn't pending.
     *
     * @private
     */
    function scheduleFlush(fastdom) {
        if (!fastdom.scheduled) {
            fastdom.scheduled = true;
            fastdom.raf(flush.bind(null, fastdom));
            debug('flush scheduled');
        }
    }

    /**
     * Runs queued `read` and `write` tasks.
     *
     * Errors are caught and thrown by default.
     * If a `.catch` function has been defined
     * it is called instead.
     *
     * @private
     */
    function flush(fastdom) {
        debug('flush');

        var writes = fastdom.writes;
        var reads = fastdom.reads;
        var error;

        try {
            debug('flushing reads', reads.length);
            fastdom.runTasks(reads);
            debug('flushing writes', writes.length);
            fastdom.runTasks(writes);
        } catch (e) { error = e; }

        fastdom.scheduled = false;

        // If the batch errored we may still have tasks queued
        if (reads.length || writes.length) scheduleFlush(fastdom);

        if (error) {
            debug('task errored', error.message);
            if (fastdom.catch) fastdom.catch(error);
            else throw error;
        }
    }

    /**
     * Remove an item from an Array.
     *
     * @param  {Array} array
     * @param  {*} item
     * @return {Boolean}
     */
    function remove(array, item) {
        var index = array.indexOf(item);
        return !!~index && !!array.splice(index, 1);
    }

    /**
     * Mixin own properties of source
     * object into the target.
     *
     * @param  {Object} target
     * @param  {Object} source
     */
    function mixin(target, source) {
        for (var key in source) {
            if (source.hasOwnProperty(key)) target[key] = source[key];
        }
    }

    // There should never be more than
    // one instance of `FastDom` in an app
    var exports = win.fastdom = (win.fastdom || new FastDom()); // jshint ignore:line

    // Expose to CJS & AMD
    if ((typeof define) == 'function') define(function () { return exports; });
    else if ((typeof module) == 'object') module.exports = exports;

})(typeof window !== 'undefined' ? window : typeof this != 'undefined' ? this : globalThis);